using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using System.Data;

namespace Dbam.DbFramework
{
	/// <summary>
	/// Provides information for the mapping of an entity to its relational data.
	/// </summary>
	internal class EntityTypeMappingInfo
	{
		private IDbFactory _dbFactory;
		private Type _entityType;
		private TableAttribute _tableInfo;
		private Dictionary<ColumnAttribute, PropertyInfo> _mappings;
		private PropertyInfo[] _mappedProperties;
		private ColumnAttribute[] _mappedColumns;
		private SortedDictionary<int, ColumnAttribute> _sortColumns;
		private IDbCommand _selectCommand;
		private IDbCommand _insertCommand;
		private IDbCommand _updateCommand;
		private IDbCommand _deleteCommand;

		public EntityTypeMappingInfo(IDbFactory dbFactory, Type entityType)
		{
			if(entityType == null)
				throw new ArgumentNullException("entityType");
			_dbFactory = dbFactory;
			_entityType = entityType;
			this.ExtractTableInfo();
			this.ExtractMappingInfo();
			this.CreateSelectCommand();
			this.CreateInsertCommand();
			this.CreateUpdateCommand();
			this.CreateDeleteCommand();
		}

		private void CreateSelectCommand()
		{
			// Setup the parameters for the command
			List<IDataParameter> cmdParams = new List<IDataParameter>();
			StringBuilder criteria = new StringBuilder();
			foreach(ColumnAttribute columnInfo in _mappedColumns)
			{
				if(columnInfo.IsPrimaryKey)
				{
					IDataParameter param = _dbFactory.CreateParam(
						"@" + columnInfo.ColumnName,
						columnInfo.DbType,
						ParameterDirection.Input,
						null);
					cmdParams.Add(param);
					criteria.Append(string.Format(@"{0}=@{0}", columnInfo.ColumnName));
					criteria.Append(",");
				}
			}
			if(criteria.Length > 0)
				criteria.Remove(criteria.Length - 1, 1);
			string cmdTextFormat = "Select * From {0} Where {1}";
			string cmdText = string.Format(cmdTextFormat, _tableInfo.TableName, criteria.ToString());
			_selectCommand = _dbFactory.CreateCommand(cmdText);
			foreach(IDataParameter param in cmdParams)
				_selectCommand.Parameters.Add(param);
		}

		private void CreateInsertCommand()
		{
			// Setup the parameters for the command
			List<IDataParameter> cmdParams = new List<IDataParameter>();
			StringBuilder fieldsList = new StringBuilder();
			StringBuilder valuesList = new StringBuilder();
			foreach(ColumnAttribute columnInfo in _mappedColumns)
			{
				IDataParameter param = _dbFactory.CreateParam(
					"@" + columnInfo.ColumnName,
					columnInfo.DbType,
					ParameterDirection.Input,
					null);
				cmdParams.Add(param);

				fieldsList.Append(string.Format("[{0}]", columnInfo.ColumnName));
				fieldsList.Append(",");
				valuesList.Append(string.Format("@{0}", columnInfo.ColumnName));
				valuesList.Append(",");
			}
			if(fieldsList.Length > 0)
			{
				fieldsList.Remove(fieldsList.Length - 1, 1);
				valuesList.Remove(valuesList.Length - 1, 1);
			}
			string cmdTextFormat = "Insert Into [{0}] ({1}) Values ({2})";
			string cmdText = string.Format(cmdTextFormat, _tableInfo.TableName, fieldsList.ToString(), valuesList.ToString());
			_insertCommand = _dbFactory.CreateCommand(cmdText);
			foreach(IDataParameter param in cmdParams)
				_insertCommand.Parameters.Add(param);
		}

		private void CreateUpdateCommand()
		{
			// Setup the parameters for the command
			List<IDataParameter> cmdParams = new List<IDataParameter>();
			StringBuilder fieldValueSets = new StringBuilder();
			StringBuilder criteria = new StringBuilder();
			foreach(ColumnAttribute columnInfo in _mappedColumns)
			{
				IDataParameter param = _dbFactory.CreateParam(
					"@" + columnInfo.ColumnName,
					columnInfo.DbType,
					ParameterDirection.Input,
					null);
				cmdParams.Add(param);
				fieldValueSets.Append(string.Format("[{0}]=@{0}", columnInfo.ColumnName));
				fieldValueSets.Append(",");
				if(columnInfo.IsPrimaryKey)
				{
					criteria.Append(string.Format(@"{0}=@{0}", columnInfo.ColumnName));
					criteria.Append(",");
				}
			}
			if(fieldValueSets.Length > 0)
				fieldValueSets.Remove(fieldValueSets.Length - 1, 1);
			if(criteria.Length > 0)
				criteria.Remove(criteria.Length - 1, 1);
			string cmdTextFormat = "Update [{0}] Set {1} Where {2}";
			string cmdText = string.Format(cmdTextFormat, _tableInfo.TableName, fieldValueSets.ToString(), criteria.ToString());
			_updateCommand = _dbFactory.CreateCommand(cmdText);
			foreach(IDataParameter param in cmdParams)
				_updateCommand.Parameters.Add(param);
		}

		private void CreateDeleteCommand()
		{
			// Setup the parameters for the command
			List<IDataParameter> cmdParams = new List<IDataParameter>();
			StringBuilder criteria = new StringBuilder();
			foreach(ColumnAttribute columnInfo in _mappedColumns)
			{
				if(columnInfo.IsPrimaryKey)
				{
					IDataParameter param = _dbFactory.CreateParam(
						"@" + columnInfo.ColumnName,
						columnInfo.DbType,
						ParameterDirection.Input,
						null);
					cmdParams.Add(param);
					criteria.Append(string.Format(@"{0}=@{0}", columnInfo.ColumnName));
					criteria.Append(",");
				}
			}
			if(criteria.Length > 0)
				criteria.Remove(criteria.Length - 1, 1);
			string cmdTextFormat = "Delete From [{0}] Where {1}";
			string cmdText = string.Format(cmdTextFormat, _tableInfo.TableName, criteria.ToString());
			_deleteCommand = _dbFactory.CreateCommand(cmdText);
			foreach(IDataParameter param in cmdParams)
				_deleteCommand.Parameters.Add(param);
		}

		private void ExtractMappingInfo()
		{
			BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.NonPublic;
			_mappings = new Dictionary<ColumnAttribute, PropertyInfo>();
			_sortColumns = new SortedDictionary<int, ColumnAttribute>();
			List<PropertyInfo> properties = new List<PropertyInfo>();
			List<ColumnAttribute> columns = new List<ColumnAttribute>();
			foreach(PropertyInfo propertyInfo in _entityType.GetProperties(bindingFlags))
			{
				// Get column info
				ColumnAttribute[] columnAttributes =
					propertyInfo.GetCustomAttributes(typeof(ColumnAttribute), false) as ColumnAttribute[];
				if(columnAttributes.Length == 0)
					continue;
				_mappings.Add(columnAttributes[0], propertyInfo);
				properties.Add(propertyInfo);
				columns.Add(columnAttributes[0]);
				if(columnAttributes[0].SortIndex > -1)
				{
					int sortIndex = columnAttributes[0].SortIndex;
					if(_sortColumns.ContainsKey(sortIndex))
						throw new InvalidOperationException(ErrorStrings.InvalidColumnSortIndexing);
					_sortColumns.Add(sortIndex, columnAttributes[0]);
				}
			}
			if(_mappings.Count == 0)
				throw new InvalidOperationException(ErrorStrings.ColumnMappingsNotFound);
			_mappedProperties = properties.ToArray();
			_mappedColumns = columns.ToArray();
		}

		private void ExtractTableInfo()
		{
			Attribute[] customAttributes = _entityType.GetCustomAttributes(typeof(TableAttribute), false) as Attribute[];
			if(customAttributes.Length == 0)
				throw new InvalidOperationException(ErrorStrings.TableMappingRequired);
			_tableInfo = customAttributes[0] as TableAttribute;
		}

		public Type EntityType
		{
			get { return _entityType; }
		}

		public TableAttribute TableInfo
		{
			get { return _tableInfo; }
		}

		public PropertyInfo GetMappedProperty(string columnName)
		{
			foreach(KeyValuePair<ColumnAttribute, PropertyInfo> kvp in _mappings)
				if(kvp.Key.ColumnName == columnName)
					return kvp.Value;
			string errorMessage = string.Format(
				ErrorStrings.PropertyMappingNotFound, columnName, _entityType.Name);
			throw new ArgumentException(errorMessage);
		}

		public PropertyInfo[] MappedProperties
		{
			get { return _mappedProperties; }
		}

		public ColumnAttribute[] MappedColumns
		{
			get { return _mappedColumns; }
		}

		internal IDbCommand SelectCommand
		{
			get { return _selectCommand; }
		}

		internal IDbCommand InsertCommand
		{
			get { return _insertCommand; }
		}

		internal IDbCommand UpdateCommand
		{
			get { return _updateCommand; }
		}

		internal IDbCommand DeleteCommand
		{
			get { return _deleteCommand; }
		}

		internal void FillCommandParams(IDbCommand command, object entity)
		{
			foreach(IDataParameter param in command.Parameters)
			{
				PropertyInfo property = this.GetMappedProperty(param.ParameterName.Substring(1));
				object value = property.GetValue(entity, null);
				if(value == null)
					value = DBNull.Value;
				param.Value = value;
			}
		}
	}
}